﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace Elentok.Common.Async
{
  /// <summary>
  /// A base implementation of the <see cref="IAsyncTask"/> interface.
  /// </summary>
  public abstract class BaseAsyncTask : IAsyncTask, IAsyncProgressNotifier
  {
    #region Fields

    private int? progress;
    private string message;
    private object data;
    private AsyncTaskState state = AsyncTaskState.Idle;
    private BackgroundWorker worker;
    private Exception exception;

    #endregion

    #region Ctor

    /// <summary>
    /// Initializes a new instance of the <see cref="BaseAsyncTask"/> class.
    /// </summary>
    protected BaseAsyncTask()
    {
      this.worker = new BackgroundWorker();
      this.worker.WorkerReportsProgress = true;
      this.worker.DoWork += Worker_DoWork;
      this.worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
      this.worker.ProgressChanged += Worker_ProgressChanged;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="BaseAsyncTask"/> class.
    /// </summary>
    /// <param name="initialMessage">The initial message.</param>
    protected BaseAsyncTask(string initialMessage) : this()
    {
      this.Message = initialMessage;
    }

    #endregion

    #region IAsyncTask Members (Properties)

    /// <summary>
    /// Gets the current progress of the task (can be null, or a value between 0 or 100, including).
    /// </summary>
    /// <value>The progress.</value>
    public int? Progress
    {
      get
      {
        return this.progress;
      }
      private set
      {
        if (this.progress == value)
          return;

        if (value.HasValue)
        {
          if (value < 0 || value > 100)
            throw new ArgumentOutOfRangeException("value", "progress value must be between 0 and 100 (including)");
        }

        this.progress = value;
        OnPropertyChanged(AsyncTaskProperties.Progress);
      }
    }

    /// <summary>
    /// Gets the message to display.
    /// </summary>
    /// <value></value>
    public string Message
    {
      get
      {
        return this.message;
      }
      private set
      {
        if (this.message == value)
          return;

        this.message = value;
        OnPropertyChanged(AsyncTaskProperties.Message);
      }
    }

    /// <summary>
    /// Gets the data object (used to transfer data from the
    /// background thread to the UI thread).
    /// </summary>
    /// <value></value>
    public object Data
    {
      get
      {
        return this.data;
      }
      private set
      {
        if (this.data == value)
          return;

        this.data = value;
        OnPropertyChanged(AsyncTaskProperties.Data);
      }
    }

    /// <summary>
    /// Gets the state of the action
    /// </summary>
    /// <value></value>
    public AsyncTaskState State
    {
      get
      {
        return this.state;
      }
      private set
      {
        if (this.state == value)
          return;

        this.state = value;
        OnPropertyChanged(AsyncTaskProperties.State);
      }
    }

    /// <summary>
    /// Gets the exception that was thrown in the background (if one was thrown, otherwise returns null).
    /// </summary>
    /// <value>The exception.</value>
    public Exception Exception
    {
      get
      {
        return this.exception;
      }
      private set
      {
        if (this.exception == value)
          return;

        this.exception = value;
        OnPropertyChanged(AsyncTaskProperties.Exception);
      }
    }

    #endregion

    #region IAsyncTask Members (Methods)

    /// <summary>
    /// Starts this task in a background thread.
    /// </summary>
    /// <exception cref="AsyncTaskAlreadyRunningException"/>
    public void Start()
    {
      if (this.state != AsyncTaskState.Idle)
        throw new AsyncTaskAlreadyRunningException();

      this.State = AsyncTaskState.Running;
      this.worker.RunWorkerAsync();
    }

    /// <summary>
    /// Cancels the task.
    /// </summary>
    public void Cancel()
    {
      if (this.State == AsyncTaskState.Running)
        this.State = AsyncTaskState.PendingCancellation;

      else if (this.State == AsyncTaskState.Idle)
        this.State = AsyncTaskState.Canceled;
    }

    #endregion

    #region INotifyPropertyChanged Members

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Raises the PropertyChanged event
    /// </summary>
    protected virtual void OnPropertyChanged(string propertyName)
    {
      PropertyChangedEventHandler handler = PropertyChanged;
      if (handler != null)
        handler(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion

    #region IAsyncProgressNotifier Members (Explicit)

    /// <summary>
    /// Notifies that the progress has changed.
    /// </summary>
    /// <param name="progress">The current progress (set to <c>null</c> if the progress is unknown).</param>
    /// <param name="message">The message.</param>
    /// <param name="data">The data (can be used to transfer information from
    /// the background thread to the UI thread).</param>
    void IAsyncProgressNotifier.NotifyProgress(int? progress, string message, object data)
    {
      this.NotifyProgressChanged(progress, message, data);
    }

    /// <summary>
    /// Notifies that the progress has changed.
    /// </summary>
    /// <param name="progress">The current progress (set to <c>null</c> if the progress is unknown).</param>
    /// <param name="message">The message.</param>
    void IAsyncProgressNotifier.NotifyProgress(int? progress, string message)
    {
      this.NotifyProgressChanged(progress, message, null);
    }

    /// <summary>
    /// Gets a value indicating whether this task is pending cancellation.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this task is pending cancellation; otherwise, <c>false</c>.
    /// </value>
    bool IAsyncProgressNotifier.IsPendingCancellation
    {
      get { return this.state == AsyncTaskState.PendingCancellation; }
    }

    #endregion

    #region Private Data Types

    private struct AsyncProgressInfo
    {
      public int? Progress;
      public string Message;
      public object Data;
    }

    #endregion

    #region Protected Methods

    /// <summary>
    /// Notifies that the progress has changed.
    /// </summary>
    /// <param name="progress">The current progress (set to <c>null</c> if the progress is unknown).</param>
    /// <param name="message">The message.</param>
    /// <param name="data">The data (can be used to transfer information from
    /// the background thread to the UI thread).</param>
    protected void NotifyProgressChanged(int? progress, string message, object data)
    {
      this.worker.ReportProgress(
        0, 
        new AsyncProgressInfo
        {
          Progress = progress,
          Message = message,
          Data = data
        });
    }

    #endregion

    #region Abstract Methods

    /// <summary>
    /// When implemented by an inheriting class, this method will run in the background.
    /// </summary>
    protected abstract void RunInBackground();

    #endregion

    #region Event Handlers (Background Worker)

    private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
      var info = (AsyncProgressInfo)e.UserState;
      this.Progress = info.Progress;
      this.Message = info.Message;
      this.Data = info.Data;
    }

    private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
      if (this.Exception != null)
        this.State = AsyncTaskState.ExceptionThrown;
      else if (this.State == AsyncTaskState.PendingCancellation)
        this.State = AsyncTaskState.Canceled;
      else if (this.State == AsyncTaskState.Running)
        this.State = AsyncTaskState.Complete;
    }

    private void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
      try
      {
        RunInBackground();
      }
      catch (Exception ex)
      {
        this.Exception = ex;
      }
    }

    #endregion
  }
}
